package fr.utbm.to52.android_bibtex.model.spec;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import my.javax.swing.event.EventListenerList;
import my.util.MyCollections;

public class EntryDefinition implements FieldSpecificationListener, FieldSpecificationsListener {

	/**
	 * Rem: all uncategorized fields are considered "optional"
	 *  and are not stored in the EntryDefinition instance.
	 */
	public static enum FieldSpecificationCategory {
		REQUIRED,
		RECOMMENDED,
	}

	private static final FieldSpecificationCategory[] fieldSpecificationCategoryValues = FieldSpecificationCategory.values();

	private final String name;

	private final Map<FieldSpecificationCategory, Set<FieldSpecification>> fieldSpecifications =
			new EnumMap<FieldSpecificationCategory, Set<FieldSpecification>>(FieldSpecificationCategory.class);
	{
		for (FieldSpecificationCategory category : fieldSpecificationCategoryValues) {
			this.fieldSpecifications.put(category, new HashSet<FieldSpecification>());
		}
	}
	public static final String FIELD_SPECIFICATIONS_PROPERTY_NAME = "fieldSpecifications";

	private final EventListenerList listeners = new EventListenerList();

	public EntryDefinition(String name) {
		assert name!=null && !name.isEmpty();

		this.name = name;
	}

	public String getName() {
		return this.name;
	}

	/**
	 * 
	 * @return a read-only view of the inner set.
	 */
	public Set<FieldSpecification> getFieldSpecificationsOfCategory(FieldSpecificationCategory category) {
		assert category!=null;

		// Don't return a direct reference, because that would allow uncontrolled adds/removes on it
		return Collections.unmodifiableSet(this.fieldSpecifications.get(category));
	}

	/**
	 * 
	 * @param category
	 * @param fieldSpecifications
	 * @return the count of elements effectively added (stops at the first fail).
	 */
	public int setFieldSpecificationsOfCategory(FieldSpecificationCategory category, Collection<FieldSpecification> fieldSpecifications) {
		assert category!=null;
		assert fieldSpecifications!=null && !MyCollections.contains(fieldSpecifications, null);

		Set<FieldSpecification> thisFieldSpecificationsOfCategory = this.fieldSpecifications.get(category);
		for (FieldSpecification fs : thisFieldSpecificationsOfCategory) {
			fs.removeFieldSpecificationListener(this);
		}
		thisFieldSpecificationsOfCategory.clear();
		int addedCount = 0;
		for (FieldSpecification fs : fieldSpecifications) {
			if (!checkedAddFieldSpecificationOfCategory(category, fs)) {
				break;
			}
			addedCount++;
		}
		// TODO what old/new values?
		fireChange(FIELD_SPECIFICATIONS_PROPERTY_NAME, null, null);
		return addedCount;
	}

	public boolean addFieldSpecificationOfCategory(FieldSpecificationCategory category, FieldSpecification fs) {
		assert category!=null;
		assert fs!=null;

		if (checkedAddFieldSpecificationOfCategory(category, fs)) {
			// TODO what old/new values?
			fireChange(FIELD_SPECIFICATIONS_PROPERTY_NAME, null, null);
			return true;
		}
		return false;
	}

	public boolean removeFieldSpecificationOfCategory(FieldSpecificationCategory category, FieldSpecification fs) {
		assert category!=null;
		assert fs!=null;

		if (checkedRemoveFieldSpecificationOfCategory(category, fs)) {
			// TODO what old/new values?
			fireChange(FIELD_SPECIFICATIONS_PROPERTY_NAME, null, null);
			return true;
		}
		return false;
	}

	private boolean checkedAddFieldSpecificationOfCategory(FieldSpecificationCategory category, FieldSpecification fs) {
		assert category!=null;
		assert fs!=null;

		if (!anyFieldSpecificationsContains(fs)) {
			this.fieldSpecifications.get(category).add(fs);
			fs.addFieldSpecificationListener(this);
			return true;
		}
		return false;
	}

	private boolean checkedRemoveFieldSpecificationOfCategory(FieldSpecificationCategory category, FieldSpecification fs) {
		assert category!=null;
		assert fs!=null;

		Set<FieldSpecification> thisFieldSpecificationsOfCategory = this.fieldSpecifications.get(category);
		if (thisFieldSpecificationsOfCategory.contains(fs)) {
			fs.removeFieldSpecificationListener(this);
			thisFieldSpecificationsOfCategory.remove(fs);
			return true;
		}
		return false;
	}

	/**
	 * 
	 * @param fs
	 * @return the category of fs relative to this EntryDefinition, or
	 *         null if fs is not categorized ("optional").
	 */
	public FieldSpecificationCategory getCategoryOf(FieldSpecification fs) {
		assert fs!=null;

		for (FieldSpecificationCategory category : fieldSpecificationCategoryValues) {
			if (this.fieldSpecifications.get(category).contains(fs)) {
				return category;
			}
		}
		// not categorized
		return null;
	}

	private boolean anyFieldSpecificationsContains(FieldSpecification fs) {
		return getCategoryOf(fs) != null;
	}

	public void addEntryDefinitionListener(EntryDefinitionListener l) {
		assert l!=null;

		this.listeners.add(EntryDefinitionListener.class, l);
	}

	public void removeEntryDefinitionListener(EntryDefinitionListener l) {
		assert l!=null;

		this.listeners.remove(EntryDefinitionListener.class, l);
	}

	public EntryDefinitionListener[] getEntryDefinitionListeners() {
		return this.listeners.getListeners(EntryDefinitionListener.class);
	}

	protected void fireChange(String propertyName, Object oldValue, Object newValue) {
		EntryDefinitionEvent event = new EntryDefinitionEvent(this, propertyName, oldValue, newValue);
		for (EntryDefinitionListener listener : getEntryDefinitionListeners()) {
			listener.entryDefinitionChanged(event);
		}
	}

	@Override
	public void fieldSpecificationChanged(FieldSpecificationEvent event) {
		FieldSpecification fs = event.getSource();

		assert anyFieldSpecificationsContains(fs);

		if (FieldSpecification.TYPE_PROPERTY_NAME.equals(event.getPropertyName())) {
			ValueType oldType = (ValueType) event.getOldValue();
			ValueType newType = (ValueType) event.getNewValue();

			// TODO handle field specification's type change
		}

		// TODO what old/new values?
		fireChange(FIELD_SPECIFICATIONS_PROPERTY_NAME, null, null);
	}

	@Override
	public void specificationAdded(FieldSpecificationsEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void specificationRemoved(FieldSpecificationsEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public void specificationChanged(FieldSpecificationsEvent event) {
		// TODO Auto-generated method stub

	}

	@Override
	public String toString() {
		return getName();
	}

}
